package com.yixing.tech.wms.service.impl;

import com.yixing.tech.common.base.BaseResponse;
import com.yixing.tech.common.base.BusinessException;
import com.yixing.tech.common.convert.ConvertUtils;
import com.yixing.tech.common.enums.YesOrNo;
import com.yixing.tech.common.service.AbstractWmsService;
import com.yixing.tech.wms.feigh.WcsService;
import com.yixing.tech.wms.service.InitService;
import com.yixing.tech.wms.service.StockLocationService;
import com.yixing.tech.wms.service.WmsTaskService;
import com.yixing.tech.wms.stock.out.StockOutConstants;
import com.yixing.tech.wms.stock.out.callback.error.ErrorCodeEnum;
import com.yixing.tech.wms.stock.out.callback.error.TaskErrorCallbackVO;
import com.yixing.tech.wms.stock.out.domain.Task;
import com.yixing.tech.wms.stock.out.domain.vo.TaskMethod;
import com.yixing.tech.wms.stock.out.domain.vo.TaskType;
import com.yixing.tech.wms.utils.DocumentUtils;
import com.mongodb.client.result.UpdateResult;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.bson.Document;
import org.bson.types.ObjectId;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.data.mongodb.core.aggregation.Aggregation;
import org.springframework.data.mongodb.core.aggregation.AggregationOperation;
import org.springframework.data.mongodb.core.aggregation.AggregationResults;
import org.springframework.data.mongodb.core.aggregation.ConvertOperators;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Update;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;

import java.util.*;
import java.util.function.Supplier;
import java.util.stream.Collectors;

import static com.yixing.tech.common.CommonConstant.*;
import static com.yixing.tech.common.CommonConstant.ContainerType.*;

/**
 * ContainerService
 *
 * @author yixing tech
 * @since 1.0.0
 */
@Slf4j
@Service
public class InitServiceImpl extends AbstractWmsService implements InitService {

    @Autowired
    private StockLocationService stockLocationService;
    @Lazy
    @Autowired
    private WcsService wcsService;
    @Autowired
    private WmsTaskService wmsTaskService;

    private void assert0(Supplier<Boolean> supplier, String message) throws BusinessException {
        if (supplier.get()) {
            throw new BusinessException(message);
        }
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean initIn(String workCode) throws BusinessException {
        assert0(() -> StringUtils.isBlank(workCode), "作业号为空");
        Document init = simpleFindOne("entity_" + INIT_IN_WORK, "workCode", workCode);
        assert0(() -> init == null, String.format("作业号[%s]不存在", workCode));
        String status = init.getString("status");
        assert0(() -> InitStatus.starting.name().equals(status) || InitStatus.running.equals(status),
                String.format("作业号[%s]已在执行中", workCode));

        // 必要字段
        String pickLocationCode = init.getString("pickLocationCode");
        assert0(() -> StringUtils.isBlank(pickLocationCode), "拣货库位为空");
        Document pickLocation = simpleFindOne("entity_" + STOCK_LOCATION, "code", pickLocationCode);
        assert0(() -> pickLocation == null, String.format("拣货库位[%s]不存在", pickLocationCode));
        String containerCode = init.getString("containerCode");
        assert0(() -> StringUtils.isBlank(containerCode), "容器码为空");
        String ab = init.getString("ab");
        assert0(() -> StringUtils.isBlank(ab), "ab库选择不能为空");
        boolean isProd = init.getBoolean("isProd");

        // 校验改库位是否存在未完成的作业
        List<Document> unfinished = mongoTemplate.find(Query.query(newCriteria()
                        .and("pickLocationCode").is(pickLocationCode)
                        .and("workCode").ne(workCode)
                        .and("status").ne(InitStatus.finished.name())),
                Document.class, "entity_" + INIT_IN_WORK);
        assert0(() -> !CollectionUtils.isEmpty(unfinished), String.format("存在未执行完成的初始化入库作业: %s", unfinished.stream().map(x -> x.getString("workCode")).collect(Collectors.joining(","))));

        // 获取并校验对象
        Document container = simpleFindOne("entity_" + CONTAINER, "code", containerCode);
        assert0(() -> container == null, String.format("容器[%s]不存在", containerCode));
        String containerType = container.getEmbedded(Arrays.asList("type", "id"), String.class);

        Document stockArea;
        List<String> pickAreaCodes;
        if(A.equals(ab)) {
            stockArea = simpleFindOne("entity_" + STOCK_AREA, "code", "100104");
            List<Document> documents = simpleFind("entity_" + STOCK_AREA, "storageType", newDocument("id", new ObjectId(AreaType.stereoscopicSorting.getCode())));
            pickAreaCodes = documents.stream().map(x -> x.getString("code")).collect(Collectors.toList());

        } else if (B.equals(ab)) {
            stockArea = simpleFindOne("entity_" + STOCK_AREA, "code", "100203");
            pickAreaCodes = Arrays.asList(AreaCodeDict.bMatPickingExit.getText());
            // 校验当前读取的容器码
            Document realCc = simpleFindOne("entity_" + CONTAINER_READ_REALTIME);
            assert0(() -> !Objects.equals(realCc.getString("containerCode"), containerCode),
                    String.format("当前相机读取到的容器码是[%s]，请等待当前容器被相机读取到再执行", realCc.getString("containerCode")));
        } else {
            stockArea = null;
            pickAreaCodes = Collections.emptyList();
        }
        assert0(() -> stockArea == null, String.format("根据ab库[%s]未找到对应的库区", ab));
        assert0(() -> !pickAreaCodes.contains(pickLocation.getString("area")),
                String.format("[%s]库拣货区有[%s]，但是当前库位[%s]所属库区是[%s]", ab, pickAreaCodes, pickLocationCode, pickLocation.get("area")));
        String stockAreaCode = stockArea.getString("code");

        // 校验物料
        List<Document> dtls = init.getList("dtls", Document.class);
        if (!CollectionUtils.isEmpty(dtls)) {
            Set<String> mcs = new HashSet<>();
            Set<String> mcsPartitionSides = new HashSet<>();
            Map<String, Set<String>> mcBatchNoMap = new HashMap<>();
            // key: 物料编码 : 隔板位置
            Map<String, Double> mcNumMap = new HashMap<>();
            dtls.forEach(x -> {
                String mc = x.getEmbedded(Arrays.asList("materialCode", "code"), String.class);
                mcs.add(mc);
                String key = mc + ":" + x.getOrDefault("partitionSide", "1");
                mcsPartitionSides.add(key);
                String batchNo = x.getString("batchNo");
                mcBatchNoMap.computeIfAbsent(mc, k -> new HashSet<>()).add(batchNo);
                double num = ConvertUtils.convertIfPossible(x.get("num"), Double.class, 0D);
                mcNumMap.put(key, mcNumMap.getOrDefault(key, 0D) + num);
            });

            int mcsSize = mcs.size();
            if(mcsSize > 1) {
                assert0(() -> partitionBox.getCode().equals(containerType) && mcsSize > 2, String.format("二分隔板容器最多混放两种物料"));
                assert0(() -> partitionBox3.getCode().equals(containerType) && mcsSize > 3, String.format("三分隔板容器最多混放三种物料"));
                assert0(() -> partitionBox4.getCode().equals(containerType) && mcsSize > 4, String.format("四分隔板容器最多混放四种物料"));
                assert0(() -> bigRack4.getCode().equals(containerType) && mcsSize > 4, String.format("四分隔板容器最多混放四种物料"));
                assert0(() -> lightweight6.getCode().equals(containerType) && mcsSize > 6, String.format("六分隔板容器最多混放四种物料"));
                assert0(() -> productBox2.getCode().equals(containerType) && mcsSize > 2, String.format("二分隔板容器最多混放四种物料"));
            }

            // 校验物料是否存在
            Map<String, Document> materialMap = new HashMap<>();
            for (String mcsPartitionSide : mcsPartitionSides) {
                String mc = mcsPartitionSide.split(":")[0];

                Document material = simpleFindOne("entity_" + MATERIAL, "code", mc);
                assert0(() -> material == null, String.format("物料[%s]不存在", mc));
                materialMap.put(mc, material);

                // 存储规则
                Document rule = simpleFindOne("entity_" + STOCK_RULE, "materialCode", mc);
                assert0(() -> rule == null, String.format("物料[%s]的存储规则不存在", mc));
                assert0(() -> !Objects.equals(rule.getEmbedded(Arrays.asList("containerType", "id"), String.class), containerType), String.format("物料[%s]的存储规则指定容器类型与当前实物容器类型不匹配", mc));
                int batchNoSize = mcBatchNoMap.get(mc).size();
                assert0(() -> batchNoSize > 1 && !rule.getBoolean("isMixedBatch"),
                        String.format("物料[%s]的存储规则指定不能混批次，当前混批次数量是: %s", mc, batchNoSize));
                assert0(() -> mcNumMap.get(mcsPartitionSide) > rule.getDouble("capacity"), String.format("物料[%s]的存储规则指定最大容量被超出", mc));
                boolean areasMatch = Optional.ofNullable(rule.getList("areas", String.class)).orElse(Collections.emptyList()).contains(stockAreaCode);
                assert0(() -> !areasMatch,
                        String.format("物料[%s]的存储规则指定存储区域与当前实物入库存储区不匹配", mc));
            }
        }

        // 申请库位
        List<Document> locations = stockLocationService.queryEmptyLocation(Arrays.asList(stockAreaCode), isProd);
        assert0(() -> CollectionUtils.isEmpty(locations), "当前库位不足以入库");
        assertBiz(() -> A.equals(ab) && locations.size() <= 4, "A库库位不足以分配");
        Document location = locations.get(0);
        // 锁库位
        Update u = new Update();
        u.set("lock", true);
        u.set("updateTime", new Date());
        boolean suc = versionUpdateById(location, u, "entity_" + STOCK_LOCATION);
        assert0(() -> !suc, "锁库位失败，重新尝试");

        // 创建任务
        if (B.equals(ab)) {
            Map<String, Object> params = new HashMap<>();
            params.put("podCode", containerCode);
            params.put("interactionState", "66c2dbfc7ba0581acbe7c47a");
            BaseResponse<Object> objectBaseResponse = wcsService.updateBoxState(params);
            assert0(() -> !objectBaseResponse.isOk(), String.format("调用WCS更新料箱状态失败"));
        }

        // 创建下架任务
        Task task = Task.builder()
                .taskCode(UUID.randomUUID().toString().replaceAll("-", ""))
                .taskType(TaskType.STOCK_IN.getId())
                .startLoc(pickLocationCode)
                .endArea(stockAreaCode)
                .endLoc(location.getString("code"))
                .podCode(containerCode)
                .business(INIT_IN_WORK_BIZ)
                .operationNo(Collections.singletonList(workCode))
                .build();
        wmsTaskService.saveTaskInfo(task);

        Update u1 = new Update();
        u1.set("status", InitStatus.starting.name());
        u1.set("updateTime", new Date());
        // 辅助信息
        u1.set("stockWarehouseCode", stockArea.getString("warehouse"));
        u1.set("stockAreaCode", stockAreaCode);
        u1.set("stockLocationCode", location.getString("code"));
        u1.set("containerTypeId", containerType);
        return updateById(init, u1, "entity_" + INIT_IN_WORK);
    }

    @Override
    public void handleCallback(String taskCode, String method, String locCode, String podCode) {
        log.info("初始化入库 处理任务回调信息:taskCode[{}],method[{}],locCode[{}],podCode[{}]", taskCode, method, locCode, podCode);

        List<AggregationOperation> operations = new ArrayList<>();
        operations.add(Aggregation.match(Criteria.where("tenantId").is(tenantId).and("isDelete").ne(YesOrNo.YES.ordinal())
                .and("taskCode").is(taskCode)));
        operations.add(Aggregation.addFields()
                .addFieldWithValue("taskType", ConvertOperators.ToString.toString("$taskType.id"))
                .addFieldWithValue("status", ConvertOperators.ToString.toString("$status.id"))
                .addFieldWithValue("performStatus", ConvertOperators.ToString.toString("$performStatus.id"))
                .build());

        AggregationResults<Task> results = mongoTemplate.aggregate(Aggregation.newAggregation(operations), StockOutConstants.TASK_TEMPLATE, Task.class);
        if (ObjectUtils.isEmpty(results.getMappedResults())) {
            return;
        }
        Task task = results.getMappedResults().get(0);
        if (TaskMethod.OUTBIN.getAction().equals(method)) {
            // 处理离开起点
            handleOutBin(taskCode, method, locCode, podCode, task);
        } else if (TaskType.STOCK_IN.getId().equals(task.getTaskType()) && TaskMethod.FINISH.getAction().equals(method)) {
            // 容器到达仓库
            containerInStock(taskCode, method, locCode, podCode, task);
        }
    }

    @Transactional(rollbackFor = Exception.class)
    public void containerInStock(String taskCode, String method, String locCode, String podCode, Task task) {
        List<String> operationNo = task.getOperationNo();
        if (!CollectionUtils.isEmpty(operationNo)) {
            Update update = new Update();
            update.set("status", InitStatus.finished.name());
            update.set("updateTime", new Date());
            mongoTemplate.updateMulti((Query.query(newCriteria().and("workCode").in(operationNo))), update, "entity_" + INIT_IN_WORK);
        }
        List<Document> initWorks = mongoTemplate.find(Query.query(newCriteria().and("workCode").in(operationNo)), Document.class, "entity_" + INIT_IN_WORK);
        Document initWork = initWorks.get(0);

        // 解锁容器
        unlockContainer(podCode);
        // 库位与容器绑定并解锁库位
        Update update3 = new Update();
        update3.set("lock", false);
        update3.set("containerCode", podCode);
        update3.set("updateTime", new Date());
        mongoTemplate.updateFirst((Query.query(newCriteria().and("code").is(locCode))), update3, "entity_" + STOCK_LOCATION);

        // 保存库存
        List<Document> stockSaveList = new LinkedList<>();
        Map<String, Object> stockBaseMap = new HashMap<>();
        stockBaseMap.put("warehouseCode", initWork.getString("stockWarehouseCode"));
        stockBaseMap.put("areaCode", initWork.getString("stockAreaCode"));
        stockBaseMap.put("locationCode", initWork.getString("stockLocationCode"));
        stockBaseMap.put("containerCode", initWork.getString("containerCode"));
        stockBaseMap.put("containerType", DocumentUtils.wrapDictItem(initWork.getString("containerTypeId")));
        stockBaseMap.put("upJobCode", initWork.getString("workCode"));
        stockBaseMap.put("source", "初始化");
        stockBaseMap.put("createTime", new Date());
        stockBaseMap.put("tenantId", tenantId);
        stockBaseMap.put("isDelete", 0);

        List<Document> dtls = initWork.getList("dtls", Document.class);
        if (CollectionUtils.isEmpty(dtls)) {
            // 空料架入库
            Document stock = new Document();
            stock.putAll(stockBaseMap);
            stockSaveList.add(stock);
        } else {
            dtls.forEach(dtl -> {
                Document stock = new Document();
                stock.putAll(stockBaseMap);
                stock.put("materialCode", dtl.getEmbedded(Arrays.asList("materialCode", "code"), String.class));
                stock.put("partitionSide", dtl.getString("partitionSide"));
                stock.put("colorName", dtl.getString("colorName"));
                stock.put("languageName", dtl.getString("languageName"));
                stock.put("batchNo", dtl.getString("batchNo"));
                stock.put("initNum", dtl.get("num"));
                stock.put("realNum", dtl.get("num"));
                stockSaveList.add(stock);
            });
        }

        mongoTemplate.insert(stockSaveList, "entity_" + STOCK_NAME);
    }

    private void unlockContainer(String containerCode) {
        Update update2 = new Update();
        update2.set("lock", false);
        update2.set("updateTime", new Date());
        mongoTemplate.updateFirst((Query.query(newCriteria().and("code").is(containerCode))), update2, "entity_" + CONTAINER);
    }

    @Transactional(rollbackFor = Exception.class)
    public void handleOutBin(String taskCode, String method, String locCode, String podCode, Task task) {
        List<String> operationNo = task.getOperationNo();
        if (!CollectionUtils.isEmpty(operationNo)) {
            Update update = new Update();
            update.set("status", InitStatus.starting.name());
            update.set("updateTime", new Date());
            mongoTemplate.updateMulti((Query.query(newCriteria().and("workCode").in(operationNo))), update, "entity_" + INIT_IN_WORK);
        }

        // 处理离开起点
        // 起点库位解绑容器，并解锁
        Update update = new Update();
        update.unset("containerCode");
        update.set("lock", false);
        UpdateResult updateResult = mongoTemplate.updateFirst(Query.query(newCriteria().and("code").is(locCode)), update,
                "entity_" + STOCK_LOCATION);
        log.info("任务离开起点节点回调,更新库位[{}]绑定的容器[{}],更新结果[{}]", locCode, update, updateResult);
    }

    @Override
    public Object handleErrorCallback(TaskErrorCallbackVO errorCallbackVO) throws BusinessException {
        ErrorCodeEnum errorCodeEnum = ErrorCodeEnum.getByCode(errorCallbackVO.getErrorCode());
        if(ObjectUtils.isEmpty(errorCodeEnum)){
            log.error("异常码不在预期范围内:errorCode[{}]", errorCallbackVO.getErrorCode());
            throw new BusinessException("异常码不在预期范围内");
        }

        Task task = wmsTaskService.findTaskByTaskCode(errorCallbackVO.getTaskCode());
        if(ObjectUtils.isEmpty(task)) {
            log.error("异常任务信息不存在:taskCode[{}]", errorCallbackVO.getTaskCode());
            throw new BusinessException("异常任务信息不存在");
        }
        Object result = handleCommonError(task);
        return result;
    }

    @Transactional(rollbackFor = Exception.class)
    public String handleCommonError(Task task) throws BusinessException {
        List<String> workCodes = task.getOperationNo();
        List<Document> initWorks = mongoTemplate.find(Query.query(newCriteria().and("workCode").in(workCodes)),
                Document.class, "entity_" + INIT_IN_WORK);
        return reApplyLocation(initWorks.get(0));
    }

    @Transactional(rollbackFor = Exception.class)
    public String reApplyLocation(Document initWork) throws BusinessException {
        try {
            String ab = initWork.getString("ab");
            boolean isProd = initWork.getBoolean("isProd");

            Document stockArea;
            String stockAreaCode = "";
            if(A.equals(ab)) {
                stockArea = simpleFindOne("entity_" + STOCK_AREA, "code", "100104");
                stockAreaCode = stockArea.getString("code");
            } else if (B.equals(ab)) {
                stockArea = simpleFindOne("entity_" + STOCK_AREA, "code", "100203");
                stockAreaCode = stockArea.getString("code");
            }

            List<Document> locations = stockLocationService.queryEmptyLocation(Arrays.asList(stockAreaCode), isProd);
            assert0(() -> CollectionUtils.isEmpty(locations), "当前库位不足以入库");
            assertBiz(() -> A.equals(ab) && locations.size() <= 4, "A库库位不足以分配");
            Document location = locations.get(0);

            // 锁库位
            Update u = new Update();
            u.set("lock", true);
            u.set("updateTime", new Date());
            boolean suc = versionUpdateById(location, u, "entity_" + STOCK_LOCATION);
            assert0(() -> !suc, "锁库位失败，重新尝试");

            // 修改
            Update u1 = new Update();
            u1.set("stockLocationCode", location.getString("code"));
            u1.set("updateTime", new Date());
            updateById(initWork, u1, "entity_" + INIT_IN_WORK);

            return location.getString("code");
        } catch (Throwable e) {
            log.error("初始化入库异常回调发生异常: ", e);
            Update u = new Update();
            u.set("status", "failed");
            u.set("updateTime", new Date());
            updateById(initWork, u, "entity_" + INIT_IN_WORK);
        }
        return null;
    }
}
